package it.tukano.io;

import it.tukano.collections.LinkedListExt;
import it.tukano.collections.ListExt;
import it.tukano.io.FileExt.InputStreamCall;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * A zip file
 * @author pgi
 */
public class Zipper {
    
    /* The zip file */
    private final FileExt FILE;

    /* The zip output stream, lazy */
    private Closeable stream;

    /* The zip file */
    private ZipFile zip;

    /**
     * Instance initializer
     */
    public Zipper(FileExt file) {
        FILE = file;
    }
    
    public void acquireZip(Zipper that) throws IOException {
        ZipOutputStream zout = getOutputStream(true);
        for (ZipEntry in : that.listZipEntries()) {
            String name = in.getName();
            if(in.isDirectory()) {
                if(!name.endsWith("/")) name = name + "/";
                ZipEntry dir = new ZipEntry(name);
                zout.putNextEntry(dir);
            } else {
                zip(name, that.get(name));
            }
        }
    }
    
    public void zip(final FileExt file) {
        file.withInputStream(new InputStreamCall<Void>() {

            public Void call(InputStream in) throws Throwable {
                zip(file.getFile().getName(), in);
                return null;
            }
        });
    }
    
    public void replace(File where, Map<String, byte[]> data) throws IOException {
        boolean replaceOriginal = where == null;
        OutputStream out = null;
        try {
            out = new FileOutputStream(where == null ? where = File.createTempFile(FILE.getFile().getName(), "zip") : where);
            ZipOutputStream zipper = new ZipOutputStream(out);
            for (ZipEntry zipEntry : listZipEntries()) {
                String name = zipEntry.getName();
                if(zipEntry.isDirectory() && !name.endsWith("/")) name = name + "/";
                zipEntry = new ZipEntry(name);
                if(data.containsKey(name)) {
                    if(data != null) {
                        zipper.putNextEntry(zipEntry);
                        zipper.write(data.get(name));
                    }
                } else {
                    zipper.putNextEntry(zipEntry);
                    if(!zipEntry.isDirectory()) {
                        zipper.write(getEntry(zipEntry.getName()));
                    }
                }
            }
            zipper.closeEntry();
            zipper.flush();
            zipper.finish();
            zipper.close();
        } finally {
            if(out != null) out.close();
        }
        if(replaceOriginal) {
            FILE.getFile().delete();
            where.renameTo(FILE.getFile());
        }
    }

    /**
     * Replaces an entry in the given zip file
     * @param where the new file built with the replacement. Can be null. If null
     * the original zip is overwritten
     * @param entryName the name of the entry to replace
     * @param data the new data fot the zip entry. If null, the entry will be
     * discarded
     * @throws IOException
     */
    public void replace(File where, String entryName, byte[] data) throws IOException {
        boolean replaceOriginal = where == null;
        OutputStream out = null;
        try {
            out = new FileOutputStream(where == null ? where = File.createTempFile(FILE.getFile().getName(), "zip") : where);
            ZipOutputStream zipper = new ZipOutputStream(out);
            for (ZipEntry zipEntry : listZipEntries()) {
                String name = zipEntry.getName();
                if(zipEntry.isDirectory() && !name.endsWith("/")) name = name + "/";
                zipEntry = new ZipEntry(name);
                if(name.equals(entryName)) {
                    if(data != null) {
                        zipper.putNextEntry(zipEntry);
                        zipper.write(data);
                    }
                } else {
                    zipper.putNextEntry(zipEntry);
                    if(!zipEntry.isDirectory()) {
                        zipper.write(getEntry(zipEntry.getName()));
                    }
                }
            }
            zipper.closeEntry();
            zipper.flush();
            zipper.finish();
            zipper.close();
        } finally {
            if(out != null) out.close();
        }
        if(replaceOriginal) {
            FILE.getFile().delete();
            where.renameTo(FILE.getFile());
        }
    }

    /**
     * Reads an entry of the zip file as a text file
     * @param name the name of the entry to read
     * @param charset the charset to use. Can be null. If null, default is used.
     * @return the content as text or null if the entry doesn't exist
     */
    public String getEntryAsText(String name, String charset) {
        byte[] entry = getEntry(name);
        if(entry != null) {
            return charset == null ?
                    Charset.defaultCharset().decode(ByteBuffer.wrap(entry)).toString() :
                    Charset.forName(charset).decode(ByteBuffer.wrap(entry)).toString();
        } else {
            return null;
        }
    }

    /**
     * Opens the zip file, reads an entry as a byte sequence, closes the zip file
     * and returns the content
     * @param name the name of the entry to read
     * @return the content of the entry or null if some io problem occurs
     */
    public byte[] getEntry(String name) {
        byte[] buffer = null;
        ZipFile file = null;
        try {
            file = new ZipFile(FILE.getFile());
            ZipEntry entry = file.getEntry(name);
            DataInputStream din = new DataInputStream(file.getInputStream(entry));
            buffer = new byte[(int) entry.getSize()];
            din.readFully(buffer);
        } catch (ZipException ex) {
            Logger.getLogger(Zipper.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(Zipper.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            if(file != null) try {
                file.close();
            } catch (IOException ex) {
                Logger.getLogger(Zipper.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return buffer;
    }

    /**
     * List the entries of this zip file
     * @return the list of entries in this zip file. Returns an empty list if
     * read fails
     */
    public List<ZipEntry> listZipEntries() {
        ListExt<ZipEntry> list = new LinkedListExt<ZipEntry>();
        try {
            zip = new ZipFile(FILE.getFile());
            Enumeration<? extends ZipEntry> entries = zip.entries();
            while(entries.hasMoreElements()) {
                list.add(entries.nextElement());
            }
        } catch(IOException ex) {
            return Collections.<ZipEntry>emptyList();
        } finally {
            if(zip != null) try {
                zip.close();
            } catch (IOException ex) {
                Logger.getLogger(Zipper.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return list;
    }

    /**
     * List the names of the entries of this zip file
     * @return the list of names of the entries in this zip file
     */
    public List<String> listEntries() {
        List<String> entries = new LinkedList<String>();
        try {
            zip = new ZipFile(FILE.getFile());
            Enumeration<? extends ZipEntry> elements = zip.entries();
            while(elements.hasMoreElements()) {
                ZipEntry e = elements.nextElement();
                entries.add(e.getName());
            }
        } catch (ZipException ex) {
            Logger.getLogger(Zipper.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(Zipper.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            if(zip != null) try {
                zip.close();
            } catch (IOException ex) {
                Logger.getLogger(Zipper.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return entries;
    }

    /**
     * Add an entry to this zip file.
     * @param entryName the name of the entry
     * @param entryData the data of the entry
     * @throws IOException if zip writing fails.
     */
    public void zip(String entryName, InputStream entryData) throws IOException {
        ZipOutputStream out = getOutputStream(true);
        ZipEntry entry = new ZipEntry(entryName);
        out.putNextEntry(entry);
        byte[] buffer = new byte[1024];
        for(int read = entryData.read(buffer); read != -1; read = entryData.read(buffer)) {
            out.write(buffer, 0, read);
        }
    }

    /**
     * Stores a class file into the zip.
     * @param c the class to store
     * @throws IOException storing fails
     */
    public void zip(Class<?> c) throws IOException {
        String resource = "/" + c.getName().replace(".", "/") + ".class";
        URL url = Zipper.class.getResource(resource);
        InputStream in = null;
        try {
            in = url.openStream();
            zip(resource, in);
        } finally {
            if(in != null) in.close();
        }
    }

    /**
     * Closes the zip file
     * @throws IOException if flushing/closing fails 
     */
    public void finishAndClose() throws IOException {
        ZipOutputStream out = getOutputStream(false);
        if(out != null) try {
            out.finish();
        } finally {
            out.close();
        }
    }

    private ZipOutputStream getOutputStream(boolean open) throws IOException {
        if(stream == null && open) {
            stream = new ZipOutputStream(FILE.getOutputStream(false));
        }
        return (ZipOutputStream) stream;
    }

    /**
     * Closes this zip file.
     * @throws IOException if closing fails
     */
    public void close() throws IOException {
        if(stream != null) {
            stream.close();
        }
    }

    /**
     * Returns a stream that reads the content of an entry
     * @param entryName the name of the entry to read
     * @return the input stream
     * @throws IOException if a io exception happens
     */
    public InputStream get(String entryName) throws IOException {
        ZipFile zipFile = getZipFile(true);
        ZipEntry entry = zipFile.getEntry(entryName);
        return zipFile.getInputStream(entry);
    }

    private ZipFile getZipFile(boolean create) throws IOException {
        if(zip == null && create) {
            zip = new ZipFile(FILE.getFile());
        }
        return zip;
    }
}
